package info.niwota.frames;

import java.io.Closeable;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import android.app.ProgressDialog;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.location.Location;
import android.net.Uri;
import android.os.Environment;
import android.os.Handler;
import android.os.ParcelFileDescriptor;
import android.provider.MediaStore.Images;
import android.provider.MediaStore.Images.ImageColumns;
import android.text.format.DateFormat;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.animation.Animation;
import android.view.animation.TranslateAnimation;

public class Util {
	private static final String TAG = "Util";
	public static final int DIRECTION_LEFT = 0;
	public static final int DIRECTION_RIGHT = 1;
	public static final int DIRECTION_UP = 2;
	public static final int DIRECTION_DOWN = 3;

	private static final int NAME_MAX_LEN = 32;

	private static OnClickListener sNullOnClickListener;

	private Util() {
	}

	// Rotates the bitmap by the specified degree.
	// If a new bitmap is created, the original bitmap is recycled.
	public static Bitmap rotate(Bitmap b, int degrees) {
		if (degrees != 0 && b != null) {
			Matrix m = new Matrix();
			m.setRotate(degrees, (float) b.getWidth() / 2, (float) b
					.getHeight() / 2);
			try {
				Bitmap b2 = Bitmap.createBitmap(b, 0, 0, b.getWidth(), b
						.getHeight(), m, true);
				if (b != b2) {
					b.recycle();
					b = b2;
				}
			} catch (OutOfMemoryError ex) {
				// We have no memory to rotate. Return the original bitmap.
			}
		}
		return b;
	}

	/*
	 * Compute the sample size as a function of minSideLength and
	 * maxNumOfPixels. minSideLength is used to specify that minimal width or
	 * height of a bitmap. maxNumOfPixels is used to specify the maximal size in
	 * pixels that is tolerable in terms of memory usage.
	 * 
	 * The function returns a sample size based on the constraints. Both size
	 * and minSideLength can be passed in as IImage.UNCONSTRAINED, which
	 * indicates no care of the corresponding constraint. The functions prefers
	 * returning a sample size that generates a smaller bitmap, unless
	 * minSideLength = IImage.UNCONSTRAINED.
	 * 
	 * Also, the function rounds up the sample size to a power of 2 or multiple
	 * of 8 because BitmapFactory only honors sample size this way. For example,
	 * BitmapFactory downsamples an image by 2 even though the request is 3. So
	 * we round up the sample size to avoid OOM.
	 */
	public static int computeSampleSize(BitmapFactory.Options options,
			int minSideLength, int maxNumOfPixels) {
		int initialSize = computeInitialSampleSize(options, minSideLength,
				maxNumOfPixels);

		int roundedSize;
		if (initialSize <= 8) {
			roundedSize = 1;
			while (roundedSize < initialSize) {
				roundedSize <<= 1;
			}
		} else {
			roundedSize = (initialSize + 7) / 8 * 8;
		}

		return roundedSize;
	}

	private static int computeInitialSampleSize(BitmapFactory.Options options,
			int minSideLength, int maxNumOfPixels) {
		double w = options.outWidth;
		double h = options.outHeight;

		int lowerBound = (maxNumOfPixels == IImage.UNCONSTRAINED) ? 1
				: (int) Math.ceil(Math.sqrt(w * h / maxNumOfPixels));
		int upperBound = (minSideLength == IImage.UNCONSTRAINED) ? 128
				: (int) Math.min(Math.floor(w / minSideLength), Math.floor(h
						/ minSideLength));

		if (upperBound < lowerBound) {
			// return the larger one when there is no overlapping zone.
			return lowerBound;
		}

		if ((maxNumOfPixels == IImage.UNCONSTRAINED)
				&& (minSideLength == IImage.UNCONSTRAINED)) {
			return 1;
		} else if (minSideLength == IImage.UNCONSTRAINED) {
			return lowerBound;
		} else {
			return upperBound;
		}
	}

	// Whether we should recycle the input (unless the output is the input).
	public static final boolean RECYCLE_INPUT = true;
	public static final boolean NO_RECYCLE_INPUT = false;

	public static Bitmap transform(Matrix scaler, Bitmap source,
			int targetWidth, int targetHeight, boolean scaleUp, boolean recycle) {
		int deltaX = source.getWidth() - targetWidth;
		int deltaY = source.getHeight() - targetHeight;
		if (!scaleUp && (deltaX < 0 || deltaY < 0)) {
			/*
			 * In this case the bitmap is smaller, at least in one dimension,
			 * than the target. Transform it by placing as much of the image as
			 * possible into the target and leaving the top/bottom or left/right
			 * (or both) black.
			 */
			Bitmap b2 = Bitmap.createBitmap(targetWidth, targetHeight,
					Bitmap.Config.ARGB_8888);
			Canvas c = new Canvas(b2);

			int deltaXHalf = Math.max(0, deltaX / 2);
			int deltaYHalf = Math.max(0, deltaY / 2);
			Rect src = new Rect(deltaXHalf, deltaYHalf, deltaXHalf
					+ Math.min(targetWidth, source.getWidth()), deltaYHalf
					+ Math.min(targetHeight, source.getHeight()));
			int dstX = (targetWidth - src.width()) / 2;
			int dstY = (targetHeight - src.height()) / 2;
			Rect dst = new Rect(dstX, dstY, targetWidth - dstX, targetHeight
					- dstY);
			c.drawBitmap(source, src, dst, null);
			if (recycle) {
				source.recycle();
			}
			return b2;
		}
		float bitmapWidthF = source.getWidth();
		float bitmapHeightF = source.getHeight();

		float bitmapAspect = bitmapWidthF / bitmapHeightF;
		float viewAspect = (float) targetWidth / targetHeight;

		if (bitmapAspect > viewAspect) {
			float scale = targetHeight / bitmapHeightF;
			if (scale < .9F || scale > 1F) {
				scaler.setScale(scale, scale);
			} else {
				scaler = null;
			}
		} else {
			float scale = targetWidth / bitmapWidthF;
			if (scale < .9F || scale > 1F) {
				scaler.setScale(scale, scale);
			} else {
				scaler = null;
			}
		}

		Bitmap b1;
		if (scaler != null) {
			// this is used for minithumb and crop, so we want to filter here.
			b1 = Bitmap.createBitmap(source, 0, 0, source.getWidth(), source
					.getHeight(), scaler, true);
		} else {
			b1 = source;
		}

		if (recycle && b1 != source) {
			source.recycle();
		}

		int dx1 = Math.max(0, b1.getWidth() - targetWidth);
		int dy1 = Math.max(0, b1.getHeight() - targetHeight);

		Bitmap b2 = Bitmap.createBitmap(b1, dx1 / 2, dy1 / 2, targetWidth,
				targetHeight);

		if (b2 != b1) {
			if (recycle || b1 != source) {
				b1.recycle();
			}
		}

		return b2;
	}

	public static <T> int indexOf(T[] array, T s) {
		for (int i = 0; i < array.length; i++) {
			if (array[i].equals(s)) {
				return i;
			}
		}
		return -1;
	}

	public static void closeSilently(Closeable c) {
		if (c == null) {
			return;
		}
		try {
			c.close();
		} catch (Throwable t) {
			// do nothing
		}
	}

	public static void closeSilently(ParcelFileDescriptor c) {
		if (c == null)
			return;
		try {
			c.close();
		} catch (Throwable t) {
			// do nothing
		}
	}

	/**
	 * Make a bitmap from a given Uri.
	 * 
	 * @param uri
	 */
    public static Bitmap makeBitmap(int minSideLength, int maxNumOfPixels,
            Uri uri, ContentResolver cr, boolean useNative) {
        ParcelFileDescriptor input = null;
        try {
            input = cr.openFileDescriptor(uri, "r");
            BitmapFactory.Options options = null;
            if (useNative) {
                options = createNativeAllocOptions();
            }
            return makeBitmap(minSideLength, maxNumOfPixels, uri, cr, input,
                    options);
        } catch (IOException e) {
            return null;
        } finally {
            closeSilently(input);
        }
    }

	public static Bitmap makeBitmap(int minSideLength, int maxNumOfPixels,
			ParcelFileDescriptor pfd, boolean useNative) {
		BitmapFactory.Options options = null;
		if (useNative) {
			options = createNativeAllocOptions();
		}
		return makeBitmap(minSideLength, maxNumOfPixels, null, null, pfd,
				options);
	}

	public static Bitmap makeBitmap(int minSideLength, int maxNumOfPixels,
			Uri uri, ContentResolver cr, ParcelFileDescriptor pfd,
			BitmapFactory.Options options) {
		try {
			if (pfd == null) {
				pfd = makeInputStream(uri, cr);
			}
			if (pfd == null) {
				return missingBitmap();
				// return null;
			}
			if (options == null) {
				options = new BitmapFactory.Options();
			}

			FileDescriptor fd = pfd.getFileDescriptor();
			options.inJustDecodeBounds = true;
			BitmapManager.instance().decodeFileDescriptor(fd, options);
			if (options.mCancel || options.outWidth == -1
					|| options.outHeight == -1) {
				return null;
			}
			options.inSampleSize = computeSampleSize(options, minSideLength,
					maxNumOfPixels);
			options.inJustDecodeBounds = false;

			options.inDither = false;
			options.inPreferredConfig = Bitmap.Config.ARGB_8888;
			
			//TODO vm size too large exception
			return BitmapManager.instance().decodeFileDescriptor(fd, options);
		} catch (OutOfMemoryError e) {
			Log.e(TAG, "Got oom exception ", e);
			return null;
		} finally {
			closeSilently(pfd);
		}
	}

	private static Bitmap missingBitmap() {
		InputStream is = null;
		try {
			is = Util.class.getResourceAsStream("/info/niwota/frames/missing.png");
			return BitmapFactory.decodeStream(is);
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			closeSilently(is);
		}
		return null;
	}

	// private static Bitmap makeBitmap(byte[] jpegData, int maxNumOfPixels) {
	// try {
	// BitmapFactory.Options options = new BitmapFactory.Options();
	// options.inJustDecodeBounds = true;
	// BitmapFactory
	// .decodeByteArray(jpegData, 0, jpegData.length, options);
	// if (options.mCancel || options.outWidth == -1
	// || options.outHeight == -1) {
	// return null;
	// }
	// options.inSampleSize = computeSampleSize(options,
	// IImage.UNCONSTRAINED, maxNumOfPixels);
	// options.inJustDecodeBounds = false;
	//
	// options.inDither = false;
	// options.inPreferredConfig = Bitmap.Config.ARGB_8888;
	// return BitmapFactory.decodeByteArray(jpegData, 0, jpegData.length,
	// options);
	// } catch (OutOfMemoryError ex) {
	// Log.e(TAG, "Got oom exception ", ex);
	// return null;
	// }
	// }

	private static ParcelFileDescriptor makeInputStream(Uri uri,
			ContentResolver cr) {
		try {
			return cr.openFileDescriptor(uri, "r");
		} catch (Exception e) {
			//e.printStackTrace();
			return null;
		}
	}

	public static synchronized OnClickListener getNullOnClickListener() {
		if (sNullOnClickListener == null) {
			sNullOnClickListener = new OnClickListener() {
				public void onClick(View v) {
				}
			};
		}
		return sNullOnClickListener;
	}

	public static void Assert(boolean cond) {
		if (!cond) {
			throw new AssertionError();
		}
	}

	public static boolean equals(String a, String b) {
		// return true if both string are null or the content equals
		return a == b || a.equals(b);
	}

	// Returns an intent which is used for "set as" menu items.
	public static Intent createSetAsIntent(IImage image) {
		Uri u = image.fullSizeImageUri();
		Intent intent = new Intent(Intent.ACTION_ATTACH_DATA);
		intent.setDataAndType(u, image.getMimeType());
		intent.putExtra("mimeType", image.getMimeType());
		return intent;
	}

	// Returns Options that set the puregeable flag for Bitmap decode.
	public static BitmapFactory.Options createNativeAllocOptions() {
		BitmapFactory.Options options = new BitmapFactory.Options();
		// options.inNativeAlloc = true;
		return options;
	}

	// public static void showFatalErrorAndFinish(
	// final Activity activity, String title, String message) {
	// DialogInterface.OnClickListener buttonListener =
	// new DialogInterface.OnClickListener() {
	// public void onClick(DialogInterface dialog, int which) {
	// activity.finish();
	// }
	// };
	// new AlertDialog.Builder(activity)
	// .setCancelable(false)
	// .setIcon(android.R.drawable.ic_dialog_alert)
	// .setTitle(title)
	// .setMessage(message)
	// .setNeutralButton(R.string.details_ok, buttonListener)
	// .show();
	// }

	public static Animation slideOut(View view, int to) {
		view.setVisibility(View.INVISIBLE);
		Animation anim;
		switch (to) {
		case DIRECTION_LEFT:
			anim = new TranslateAnimation(0, -view.getWidth(), 0, 0);
			break;
		case DIRECTION_RIGHT:
			anim = new TranslateAnimation(0, view.getWidth(), 0, 0);
			break;
		case DIRECTION_UP:
			anim = new TranslateAnimation(0, 0, 0, -view.getHeight());
			break;
		case DIRECTION_DOWN:
			anim = new TranslateAnimation(0, 0, 0, view.getHeight());
			break;
		default:
			throw new IllegalArgumentException(Integer.toString(to));
		}
		anim.setDuration(500);
		view.startAnimation(anim);
		return anim;
	}

	public static Animation slideIn(View view, int from) {
		view.setVisibility(View.VISIBLE);
		Animation anim;
		switch (from) {
		case DIRECTION_LEFT:
			anim = new TranslateAnimation(-view.getWidth(), 0, 0, 0);
			break;
		case DIRECTION_RIGHT:
			anim = new TranslateAnimation(view.getWidth(), 0, 0, 0);
			break;
		case DIRECTION_UP:
			anim = new TranslateAnimation(0, 0, -view.getHeight(), 0);
			break;
		case DIRECTION_DOWN:
			anim = new TranslateAnimation(0, 0, view.getHeight(), 0);
			break;
		default:
			throw new IllegalArgumentException(Integer.toString(from));
		}
		anim.setDuration(500);
		view.startAnimation(anim);
		return anim;
	}

	private static class BackgroundJob extends
			MonitoredActivity.LifeCycleAdapter implements Runnable {

		private final MonitoredActivity mActivity;
		private final ProgressDialog mDialog;
		private final Runnable mJob;
		private final Handler mHandler;
		private final Runnable mCleanupRunner = new Runnable() {
			public void run() {
				mActivity.removeLifeCycleListener(BackgroundJob.this);
				if (mDialog.getWindow() != null)
					mDialog.dismiss();
			}
		};

		public BackgroundJob(MonitoredActivity activity, Runnable job,
				ProgressDialog dialog, Handler handler) {
			mActivity = activity;
			mDialog = dialog;
			mJob = job;
			mActivity.addLifeCycleListener(this);
			mHandler = handler;
		}

		public void run() {
			try {
				mJob.run();
			} finally {
				mHandler.post(mCleanupRunner);
			}
		}

		@Override
		public void onActivityDestroyed(MonitoredActivity activity) {
			// We get here only when the onDestroyed being called before
			// the mCleanupRunner. So, run it now and remove it from the queue
			mCleanupRunner.run();
			mHandler.removeCallbacks(mCleanupRunner);
		}

		@Override
		public void onActivityStopped(MonitoredActivity activity) {
			mDialog.hide();
		}

		@Override
		public void onActivityStarted(MonitoredActivity activity) {
			mDialog.show();
		}
	}

	public static void startBackgroundJob(MonitoredActivity activity,
			String title, String message, Runnable job, Handler handler) {
		// Make the progress dialog uncancelable, so that we can gurantee
		// the thread will be done before the activity getting destroyed.
		ProgressDialog dialog = ProgressDialog.show(activity, title, message,
				true, false);
		new Thread(new BackgroundJob(activity, job, dialog, handler)).start();
	}

	public static String getDataPath(Context ctx, Uri uri) {
		if (uri != null) {
			Cursor c = ctx.getContentResolver().query(uri,
					new String[] { ImageColumns.DATA }, null, null, null);
			c.moveToFirst();
			String p = c.getString(0);
			c.close();
			if (Debug.DEBUG) {
				Log.d(TAG, "getDataPath " + p);
			}
			return p;
		}
		return null;
	}
	
	public static File newFile(Context ctx, File root, String name) {
		OutputStream os = null;
		try {
			String n = normalize(name);
			File file = new File(root, n);
			file = findUniqueFile(file);
			os = new FileOutputStream(file);
			return file;
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			closeSilently(os);
		}
		return null;
	}
	
	/**
	 * create if it does not exist
	 * @param ctx
	 * @param root
	 * @param peer
	 * @param name
	 * @return
	 */
	public static File ensurePeerFile(Context ctx, File root, String peer, String name) {
		OutputStream os = null;
		try {
			File file = new File(root, "~" + peer + " " + name);
			if (file.exists()) {
				return file;
			}
			File p = file.getParentFile();
			if (!p.exists()) {
				p.mkdirs();
			}
			os = new FileOutputStream(file);
			return file;
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			closeSilently(os);
		}
		return null;
	}
	
	/**
	 * 1. replace lf/cr with space 
	 * 2. trim leading/trailing spaces
	 * 3. remove leading ~ (reserved for peer, ~jid@domain label)
	 * @param s
	 * @return
	 */
	private static String normalize(String s) {
		if (s == null || s.length() == 0) {
			return "";
		}
		//
		final String regex = "[\\s]+";
		if (s.indexOf(0) == '~') {
			s = s.substring(1);
		}
		return s.replaceAll(regex, " ").trim();
	}

	public static File findUniqueFile(File file) {
		if (!file.exists()) {
	
			return file;
		}
		File dir = file.getParentFile();
		String name = file.getName();
		String ext = "";
		//
		int idx = name.lastIndexOf(".");
		if (idx > 0) {
			ext = name.substring(idx); //including dot
			name = name.substring(0, idx);
		}
		// Try until we find a filename which
		// does not exist yet.
		
		for (int i = 0; i < Integer.MAX_VALUE; i++) {
			File candidate = new File(dir, name + "-" + i + ext);
			boolean exists = candidate.exists();
			if (!exists) {
				return candidate;
			}
		}	
		return null;
	}

	private static final Uri STORAGE_URI = Images.Media.EXTERNAL_CONTENT_URI;

	public static Uri addImage(ContentResolver cr, File file, String title, String displayName, String desc) {
		return addImage(cr, file, title, displayName, desc, System.currentTimeMillis(), null);
	}

	public static Uri addImage(ContentResolver cr, File file, String title, String displayName, String desc,
			long dateTaken, Location location) {
		//
		ContentValues values = new ContentValues(8);
		values.put(Images.Media.TITLE, title);
		values.put(Images.Media.DESCRIPTION, desc);
		values.put(Images.Media.DISPLAY_NAME, displayName);
		values.put(Images.Media.DATE_TAKEN, dateTaken);
		values.put(Images.Media.MIME_TYPE, "image/png");
		values.put(Images.Media.ORIENTATION, 0);
		values.put(Images.Media.SIZE, file.length());
		values.put(Images.Media.DATA, file.getPath());
		if (location != null) {
			values.put(Images.Media.LATITUDE, location.getLatitude());
			values.put(Images.Media.LONGITUDE, location.getLongitude());
		}
		
		return cr.insert(STORAGE_URI, values);
	}
	
	public static File getOutputFile(Uri uri, String fileExt) {
		File parent = getFramesArchiveRoot();
		String name = createName(uri);
		if (name == null) {
			name = createDateName(); // date
		}
		//
		File f = new File(parent, name + fileExt);
		File uf = findUniqueFile(f);

		return uf;
	}
	
	public static String createDateName() {
		long date = System.currentTimeMillis();
		return DateFormat.format("yyyy-MM-dd_kkmmss", date).toString();
	}
	
	/**
	 * remove anything after the last dot (.jpg) or dash
	 * replace lf/cr with "-"
	 * trim leading/trailing spaces
	 * truncate if longer than 32
	 * 
	 * @param s
	 * @return
	 */
	private static String normalizeName(String n) {
		if (n == null) {
			return null;
		}
		int idx = n.lastIndexOf(".");
		n = (idx > 0 ? n.substring(0, idx) : n);
		idx = n.lastIndexOf("-");
		n = (idx > 0 ? n.substring(0, idx) : n);
		//
		n = n.trim();
		final String regex = "[\\s]+";
		n = n.replaceAll(regex, "-");
		n = truncate(n, NAME_MAX_LEN);
		return n;
	}

	public static String truncate(String s, int max) {
		if (s == null) {
			return s;
		}
		int len = s.length();
		return (len > max ? s.substring(0, max) : s);
	}
	
	private static String createName(Uri uri) {
		String pn = getName(uri.getPath(), 2);
		if (pn != null) {
			return normalizeName(pn);
		}
		
		return null;
	}
	
	/**
	 * Return last part of the path if at least longer than the mix specified
	 * 
	 * @param p
	 * @param min
	 * @return
	 */
	private static String getName(String p, int min) {
		if (p == null) {
			return null;
		}
		int len = p.length();
		if (len < min) {
			return null;
		}
		int start = (p.endsWith("/") ? len - 2 : len - 1);
		int idx = p.lastIndexOf("/", start);
		p = (idx ==  -1 ? p : p.substring(idx + 1, start + 1));
		return (p.length() < min ? null : p);
	}
	
	private static final String ARCHIVE_ROOT = "info.niwota.frames/archive";

	public static File getFramesArchiveRoot() {
		File sdc = Environment.getExternalStorageDirectory();
		File dir = new File(sdc, ARCHIVE_ROOT);
		if (!dir.isDirectory()) {
			if (!dir.mkdirs()) {
				return null;
			}
		}
		return dir;
	}
	
	public static void save(Context ctx, File file, Drawable image,
			CompressFormat cf) throws IOException {
		FileOutputStream fos = null;
		try {
			fos = new FileOutputStream(file);
			
			Bitmap bitmap = null;
			if (image instanceof BitmapDrawable) {
				bitmap = ((BitmapDrawable) image).getBitmap();
			} else {
				final Config bc = Bitmap.Config.ARGB_8888;
				final int w = image.getIntrinsicWidth();
				final int h = image.getIntrinsicHeight();
				bitmap = Bitmap.createBitmap(w, h, bc);
				Canvas c = new Canvas(bitmap);
				//c.drawColor(Color.WHITE);
				image.setBounds(0, 0, w, h);
				image.setAlpha(100);
				image.setDither(true);
				image.draw(c);
			}
			//
			final int q = 100;
			bitmap.compress(cf, q, fos);
		} finally {
			if (fos != null) {
				fos.close();
			}
		}
	}
}
